Список Задач + Dependency Inversion Principle
➡️Ссылка на репозиторий с кодом этого урока
Создание контракта (Интерфейса)
Добавим новый файл для интерфейса. Он будет определять все публичные методы, которые должен иметь любой сервис хранения данных в приложении.
Файл lib/services/storage_interface.dart
import '../models/task.dart';
// Префикс 'I' (IStorageService) - соглашение для именования интерфейсов.
abstract interface class IStorageService {
Future<bool> getThemeMode();
Future<void> saveThemeMode(bool isDarkMode);
Future<List<Task>> getAllTasks();
Future<Task> createTask(String text);
Future<void> updateTask(Task task);
Future<void> deleteTask(int id);
}
Теперь любой класс, который захочет выступать в роли сервиса хранения, должен будет реализовать (implements) этот интерфейс и предоставить свою версию каждого из этих четырёх методов.
Реализация контракта
Переименуем StorageService в SharedPreferencesService и заставим его "подписать" контракт с помощью ключевого слова implements.

Файл lib/services/shared_preferences_service.dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:todo_shared_preferences_2025/services/storage_interface.dart';
import '../models/task.dart';
class SharedPreferencesService implements IStorageService {
static const taskPrefix = 'todo_'; // префикс для ключей задач
static const themeKey = 'isDarkMode'; // ключ для цветовой темы
/// Сохранить значение цветовой темы
@override
Future<void> saveThemeMode(bool isDarkMode) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(themeKey, isDarkMode);
} catch (e) {
throw Exception('Ошибка при сохранении темы: $e');
}
}
/// Получить значение сохранённой цветовой темы
@override
Future<bool> getThemeMode() async {
try {
final prefs = await SharedPreferences.getInstance();
// Возвращаем сохранённое значение или false, если ничего не найдено
return prefs.getBool(themeKey) ?? false;
} catch (e) {
throw Exception('Ошибка при загрузке темы: $e');
}
}
// МЕТОДЫ ДЛЯ РАБОТЫ СО СПИСКОМ ЗАДАЧ
/// Получить список всех задач
@override
Future<List<Task>> getAllTasks() async {
try {
final prefs = await SharedPreferences.getInstance();
// Получить все ключи из хранилища
final allKeys = prefs.getKeys();
// Найти ключи которые относятся к задачам
final taskKeys = allKeys.where((key) => key.startsWith(taskPrefix));
final List<Task> tasks = [];
// 1 Перебираем ключи задач в цикле
// 2 По ключу получаем значение задачи из хранилища
// 3 Декодируем JSON обратно в Map и создаём объект Task
// 4 Добавляем объект Task в список и потом возвращаем его
for (final key in taskKeys) {
final taskJsonString = prefs.getString(key);
if (taskJsonString != null) {
// Декодируем JSON обратно в Map
final taskMap = jsonDecode(taskJsonString);
// Создаем объект Task из Map и добавляем в список
tasks.add(Task.fromJson(taskMap));
}
}
return tasks;
} catch (e) {
throw Exception('Ошибка при загрузке всех задач: $e');
}
}
// Вспомогательный метод для генерации ключа по ID задачи
String _getTaskKey(int id) => '$taskPrefix$id';
/// Создать новую задачу
@override
Future<Task> createTask(String text) async {
try {
// Получаем список всех задач
final allTasks = await getAllTasks();
int maxId = 0;
// Находим максимальный ID
for (var task in allTasks) {
if (task.id > maxId) {
maxId = task.id;
}
}
// Генерируем новый ID, если список задач пуст то id = 1
final newId = allTasks.isNotEmpty ? maxId + 1 : 1;
// Создаем новую задачу с заданным ID
final newTask = Task(id: newId, text: text, isDone: false);
// Сохраняем новую задачу в хранилище
final prefs = await SharedPreferences.getInstance();
final key = _getTaskKey(newTask.id);
final taskJson = jsonEncode(newTask.toJson());
await prefs.setString(key, taskJson);
return newTask;
} catch (e) {
throw Exception('Ошибка при создании задачи: $e');
}
}
/// Обновить существующую задачу
@override
Future<void> updateTask(Task task) async {
try {
final prefs = await SharedPreferences.getInstance();
// Фомируем ключ для локального хранилища
final key = _getTaskKey(task.id);
// Кодируем обновлённый объект Task в JSON
final taskJson = jsonEncode(task.toJson());
// Перезаписываем значение по существующему ключу
await prefs.setString(key, taskJson);
} catch (e) {
throw Exception('Ошибка при обновлении задачи с ID ${task.id}: $e');
}
}
/// Удалить задачу по её ID
@override
Future<void> deleteTask(int id) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = _getTaskKey(id);
await prefs.remove(key);
} catch (e) {
throw Exception('Ошибка при удалении задачи с ID $id: $e');
}
}
}
Модификация ViewModel для Внедрения Зависимостей
Теперь самая важная часть. "Отвяжем" ToDoViewModel от конкретной реализации. Он будет требовать в своем конструкторе любой объект, который соответствует контракту IStorageService.

Файл lib/viewmodels/todo_viewmodel.dart
import 'package:flutter/material.dart';
import '../models/task.dart';
import '../services/storage_interface.dart';
class ToDoViewModel extends ChangeNotifier {
// 👉 ViewModel больше не знает SharedPreferences это или SQLite.
// 👉 Она знает только о контракте IStorageService.
final IStorageService _storageService;
// Добавляем TextEditingController в ViewModel
final TextEditingController textEditingController = TextEditingController();
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
// Состояние для списка задач
List<Task> _tasks = [];
List<Task> get tasks => _tasks;
// 👉 ЗАВИСИМОСТЬ ИНЪЕКТИРУЕТСЯ ЗДЕСЬ через конструктор
ToDoViewModel({required IStorageService storageService})
: _storageService = storageService {
loadTasks();
}
/// Переключение цветовой темы
void toggleTheme() {
// Изменяем состояние
_isDarkMode = !_isDarkMode;
// Сохраняем значение в SharedPreferences
_storageService.saveThemeMode(_isDarkMode);
// Уведомляем слушателей, чтобы они перестраивались
notifyListeners();
}
/// Загрузить все задачи из сервиса
Future<void> loadTasks() async {
_tasks = await _storageService.getAllTasks();
_isDarkMode = await _storageService.getThemeMode();
// Уведомляем UI, что данные изменились и нужно перерисоваться
notifyListeners();
}
/// Добавить новую задачу.
Future<void> addTask(String text) async {
if (text.isEmpty) return;
// Создаём новую задачу, сохраняем в хранилище
final newTask = await _storageService.createTask(text);
// Добавляем в список _tasks для отображения в UI
_tasks.add(newTask);
// Уведомляем UI, что данные изменились и нужно перерисоваться
notifyListeners();
}
/// Обновить статус выполнения задачи
Future<void> updateTaskStatus(int id, bool isDone) async {
final taskIndex = _tasks.indexWhere((task) => task.id == id);
// Создать копию задачи с обновленным статусом
final updatedTask = _tasks[taskIndex].copyWith(isDone: isDone);
_tasks[taskIndex] = updatedTask;
notifyListeners();
await _storageService.updateTask(updatedTask);
}
/// Удалить задачу по ID.
Future<void> deleteTask(int id) async {
_tasks.removeWhere((task) => task.id == id);
notifyListeners();
// Удаляем из хранилища
await _storageService.deleteTask(id);
}
}